/*
 * Copyright (C) 2021 Huawei Device Co., Ltd. 2012-2020. All rights reserved.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package com.ablanco.zoomy;

import ohos.agp.components.Component;
import ohos.agp.components.VelocityDetector;
import ohos.app.Context;
import ohos.eventhandler.EventHandler;
import ohos.eventhandler.EventRunner;
import ohos.eventhandler.InnerEvent;
import ohos.multimodalinput.event.MmiPoint;
import ohos.multimodalinput.event.TouchEvent;

/**
 * GestureDetector
 *
 * @since 2021-07-14
 */
public class GestureDetector {
    /**
     * 监听
     *
     * @since 2021-07-14
     */
    public interface OnGestureListener {
        /**
         * onDown事件
         *
         * @param ev ev
         * @since 2021-07-14
         * @return 是否按下
         */
        boolean onDown(TouchEvent ev);

        /**
         * onShowPress事件
         *
         * @param ev ev
         * @since 2021-07-14
         */
        void onShowPress(TouchEvent ev);

        /**
         * onSingleTapUp事件
         *
         * @param ev ev
         * @return 是否onSingleTapUp
         * @since 2021-07-14
         */
        boolean onSingleTapUp(TouchEvent ev);

        /**
         * onScroll事件
         *
         * @param e1        e1
         * @param e2        e2
         * @param distanceX distanceX
         * @param distanceY distanceY
         * @return 是否onScroll
         * @since 2021-07-14
         */
        boolean onScroll(TouchEvent e1, TouchEvent e2, float distanceX, float distanceY);

        /**
         * onLongPress事件
         *
         * @param ev ev
         * @since 2021-07-14
         */
        void onLongPress(TouchEvent ev);

        /**
         * onFling事件
         *
         * @param e1        e1
         * @param e2        e2
         * @param velocityX velocityX
         * @param velocityY velocityY
         * @return 是否onFling
         * @since 2021-07-14
         */
        boolean onFling(TouchEvent e1, TouchEvent e2, float velocityX, float velocityY);
    }

    /**
     * 双击监听
     *
     * @since 2021-07-14
     */
    public interface OnDoubleTapListener {
        /**
         * onSingleTapConfirmed
         *
         * @param ev ev
         * @return 是否单击
         * @since 2021-07-14
         */
        boolean onSingleTapConfirmed(TouchEvent ev);

        /**
         * onDoubleTap
         *
         * @param ev ev
         * @return 是否双击
         * @since 2021-07-14
         */
        boolean onDoubleTap(TouchEvent ev);

        /**
         * onDoubleTapEvent
         *
         * @param ev ev
         * @return 是否双击
         * @since 2021-07-14
         */
        boolean onDoubleTapEvent(TouchEvent ev);
    }


    /**
     * Button constant: Secondary button (right mouse button).
     */
    private static final int TOUCH_GESTURE_LONG_PRESS = 1;

    private static final int TOUCH_GESTURE_DEEP_PRESS = 2;

    private int mTouchSlopSquare;
    private int mDoubleTapTouchSlopSquare;
    private int mDoubleTapSlopSquare;

    private int mMinimumFlingVelocity;
    private int mMaximumFlingVelocity;

    private static final int LONGPRESS_TIMEOUT = 500;
    private static final int TAP_TIMEOUT = 100;
    private static final int DOUBLE_TAP_TIMEOUT = 300;
    private static final int DOUBLE_TAP_MIN_TIME = 40;

    // constants for Message.what used by GestureHandler below
    private static final int SHOW_PRESS = 1;
    private static final int LONG_PRESS = 2;
    private static final int TAP = 3;

    private final EventHandler mHandler;

    private final OnGestureListener mListener;
    private OnDoubleTapListener mDoubleTapListener;

    private boolean mStillDown;
    private boolean mDeferConfirmSingleTap;
    private boolean mInLongPress;
    private boolean mInContextClick;

    private boolean mAlwaysInTapRegion;
    private boolean mAlwaysInBiggerTapRegion;
    private boolean mIgnoreNextUpEvent;

    private TouchEvent mCurrentDownEvent;

    /**
     * True when the user is still touching for the second tap (down, move, and
     * up events). Can only be true if there is a double tap listener attached.
     */
    private boolean mIsDoubleTapping;

    private float mLastFocusX;
    private float mLastFocusY;
    private float mDownFocusX;
    private float mDownFocusY;

    private boolean mIsLongpressEnabled;

    private long mDownTime;
    private long mLastUpTime;

    private MmiPoint mDownPoint;
    private MmiPoint mLastUpPoint;

    /**
     * Determines speed during touch scrolling
     */
    private VelocityDetector mVelocityDetector;

    private class GestureHandler extends EventHandler {
        GestureHandler() {
            super(EventRunner.current());
        }

        GestureHandler(EventHandler handler) {
            super(handler.getEventRunner());
        }

        @Override
        public void processEvent(InnerEvent msg) {
            switch (msg.eventId) {
                case SHOW_PRESS:
                    mListener.onShowPress(mCurrentDownEvent);
                    break;

                case LONG_PRESS:
                    dispatchLongPress();
                    break;

                case TAP:
                    // If the user's finger is still down, do not count it as a tap
                    if (mDoubleTapListener != null) {
                        if (!mStillDown) {
                            mDoubleTapListener.onSingleTapConfirmed(mCurrentDownEvent);
                        } else {
                            mDeferConfirmSingleTap = true;
                        }
                    }
                    break;

                default:
                    break;
            }
        }
    }

    /**
     * SimpleOnGestureListener
     */
    public static class SimpleOnGestureListener
        implements OnGestureListener, OnDoubleTapListener {

        /**
         * onSingleTapUp
         *
         * @param ev ev
         * @return 是否
         */
        public boolean onSingleTapUp(TouchEvent ev) {
            return false;
        }

        /**
         * onLongPress
         *
         * @param ev ev
         */
        public void onLongPress(TouchEvent ev) {
        }

        /**
         * onScroll
         *
         * @param e1        e1
         * @param e2        e2
         * @param distanceX distanceX
         * @param distanceY distanceY
         * @return boolean
         */
        public boolean onScroll(TouchEvent e1, TouchEvent e2, float distanceX, float distanceY) {
            return false;
        }

        /**
         * onFling
         *
         * @param ev   ev
         * @param touchEvent touchEvent
         * @param velocityX velocityX
         * @param velocityY velocityY
         * @return boolean
         */
        public boolean onFling(TouchEvent ev, TouchEvent touchEvent, float velocityX, float velocityY) {
            return false;
        }

        /**
         * onShowPress
         *
         * @param ev ev
         */
        public void onShowPress(TouchEvent ev) {
        }

        /**
         * onDown
         *
         * @param ev ev
         * @return boolean
         */
        public boolean onDown(TouchEvent ev) {
            return false;
        }

        /**
         * onDoubleTap
         *
         * @param ev ev
         * @return boolean
         */
        public boolean onDoubleTap(TouchEvent ev) {
            return false;
        }

        /**
         * onDoubleTapEvent
         *
         * @param ev ev
         * @return boolean
         */
        public boolean onDoubleTapEvent(TouchEvent ev) {
            return false;
        }

        /**
         * onSingleTapConfirmed
         *
         * @param ev ev
         * @return boolean
         */
        public boolean onSingleTapConfirmed(TouchEvent ev) {
            return false;
        }
    }

    /**
     * GestureDetector 构造方法
     *
     * @param context 上下文
     * @param listener 监听
     */
    public GestureDetector(Context context, OnGestureListener listener) {
        this(context, listener, null);
    }


    /**
     * GestureDetector 构造方法
     *
     * @param context  上下文
     * @param listener 监听
     * @param handler  handler
     */
    public GestureDetector(Context context, OnGestureListener listener, EventHandler handler) {
        if (handler != null) {
            mHandler = new GestureHandler(handler);
        } else {
            mHandler = new GestureHandler();
        }
        mListener = listener;
        if (listener instanceof OnDoubleTapListener) {
            setOnDoubleTapListener((OnDoubleTapListener) listener);
        }

        init(context);
    }


    private void init(Context context) {
        if (mListener == null) {
            throw new NullPointerException("OnGestureListener must not be null");
        }
        mIsLongpressEnabled = true;

        // Fallback to support pre-donuts releases
        int touchSlop;
        int doubleTapSlop;
        int doubleTapTouchSlop;
        touchSlop = 8;
        doubleTapTouchSlop = 8;
        doubleTapSlop = 100;
        mMinimumFlingVelocity = 50;
        mMaximumFlingVelocity = 5000;

        mTouchSlopSquare = touchSlop * touchSlop;
        mDoubleTapTouchSlopSquare = doubleTapTouchSlop * doubleTapTouchSlop;
        mDoubleTapSlopSquare = doubleTapSlop * doubleTapSlop;
    }

    /**
     * 设置双击监听
     *
     * @param onDoubleTapListener onDoubleTapListener
     */
    public void setOnDoubleTapListener(OnDoubleTapListener onDoubleTapListener) {
        mDoubleTapListener = onDoubleTapListener;
    }


    /**
     * onTouchEvent
     *
     * @param ev        ev
     * @param component component
     * @return boolean
     */
    public boolean onTouchEvent(TouchEvent ev, Component component) {
        final int action = ev.getAction();

        if (mVelocityDetector == null) {
            mVelocityDetector = VelocityDetector.obtainInstance();
        }
        mVelocityDetector.addEvent(ev);
        final boolean pointerUp = (action & 0xff) == TouchEvent.OTHER_POINT_UP;
        final int skipIndex = pointerUp ? ev.getIndex() : -1;

        float sumX = 0;
        float sumY = 0;
        final int count = ev.getPointerCount();
        for (int i = 0; i < count; i++) {
            if (skipIndex == i) {
                continue;
            }
            sumX += Compat.getTouchX(ev, i, component);
            sumY += Compat.getTouchY(ev, i, component);
        }
        final int div = pointerUp ? count - 1 : count;
        final float focusX = sumX / div;
        final float focusY = sumY / div;
        boolean handled = false;

        switch (action & 0xff) {
            case TouchEvent.OTHER_POINT_DOWN:
                otherPointDown(focusX, focusY);
                break;
            case TouchEvent.OTHER_POINT_UP:
                mDownFocusX = mLastFocusX = focusX;
                mDownFocusY = mLastFocusY = focusY;
                break;
            case TouchEvent.PRIMARY_POINT_UP:
                handled = primaryPointUp(ev, component, handled);
                break;
            case TouchEvent.PRIMARY_POINT_DOWN:
                handled = pointDown(ev, component, focusX, focusY, handled);
                break;
            case TouchEvent.POINT_MOVE:
                if (mInLongPress || mInContextClick) {
                    break;
                }
                handled = pointMove(ev, focusX, focusY, handled);
                break;
            case TouchEvent.CANCEL:
                cancel();
                break;
            default:
                break;
        }

        return handled;
    }

    private void otherPointDown(float focusX, float focusY) {
        mDownFocusX = mLastFocusX = focusX;
        mDownFocusY = mLastFocusY = focusY;
        cancelTaps();
    }

    private boolean primaryPointUp(TouchEvent ev, Component component, boolean handled) {
        mStillDown = false;

        if (mIsDoubleTapping) {
            // Finally, give the up event of the double-tap
            handled = mDoubleTapListener.onDoubleTapEvent(ev);
        } else if (mInLongPress) {
            mHandler.removeEvent(TAP);
            mInLongPress = false;
        } else if (mAlwaysInTapRegion && !mIgnoreNextUpEvent) {
            handled = isHandled(ev);
        } else if (!mIgnoreNextUpEvent) {
            handled = isHandled(ev, handled);
        }
        mLastUpTime = ev.getOccurredTime();
        mLastUpPoint = Compat.getTouchPoint(ev, 0, component);
        // This may have been cleared when we called out to the
        // application above.
        calledOut();
        return handled;
    }

    private boolean pointMove(TouchEvent ev, float focusX, float focusY, boolean handled) {
        final float scrollX = mLastFocusX - focusX;
        final float scrollY = mLastFocusY - focusY;

        if (mIsDoubleTapping) {
            handled |= mDoubleTapListener.onDoubleTapEvent(ev);
        } else if (mAlwaysInTapRegion) {
            final int deltaX = (int) (focusX - mDownFocusX);
            final int deltaY = (int) (focusY - mDownFocusY);
            int distance = (deltaX * deltaX) + (deltaY * deltaY);
            if (distance > mTouchSlopSquare) {
                handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
                mLastFocusX = focusX;
                mLastFocusY = focusY;
                mAlwaysInTapRegion = false;
                mHandler.removeEvent(SHOW_PRESS);
                mHandler.removeEvent(LONG_PRESS);
                mHandler.removeEvent(TAP);
            }
            if (distance > mDoubleTapTouchSlopSquare) {
                mAlwaysInBiggerTapRegion = false;
            }
        } else if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1)) {
            handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
            mLastFocusX = focusX;
            mLastFocusY = focusY;
        }
        return handled;
    }

    private boolean pointDown(TouchEvent ev, Component component, float focusX, float focusY, boolean handled) {
        if (mDoubleTapListener != null) {
            boolean hadTapMessage = mHandler.hasInnerEvent(TAP);
            if (hadTapMessage) {
                mHandler.removeEvent(TAP);
            }
            if ((mDownPoint != null)
                && (mLastUpPoint != null)
                && hadTapMessage
                && isConsideredDoubleTap(mDownPoint, mDownTime, mLastUpPoint, mLastUpTime)) {
                // This is a second tap
                mIsDoubleTapping = true;
                // Give a callback with the first tap of the double-tap
                handled |= mDoubleTapListener.onDoubleTap(mCurrentDownEvent);
                // Give a callback with down event of the double-tap
                handled |= mDoubleTapListener.onDoubleTapEvent(ev);
            } else {
                // This is a first tap
                mHandler.sendEvent(TAP, DOUBLE_TAP_TIMEOUT);
            }
        }
        mDownFocusX = mLastFocusX = focusX;
        mDownFocusY = mLastFocusY = focusY;
        if (mCurrentDownEvent != null) {
            mCurrentDownEvent = null;
        }
        mDownTime = ev.getOccurredTime();
        mDownPoint = Compat.getTouchPoint(ev, 0, component);
        mCurrentDownEvent = ev;

        mAlwaysInTapRegion = true;
        mAlwaysInBiggerTapRegion = true;
        mStillDown = true;
        mInLongPress = false;
        mDeferConfirmSingleTap = false;

        if (mIsLongpressEnabled) {
            mHandler.removeEvent(LONG_PRESS);
            mHandler.sendTimingEvent(
                InnerEvent.get(LONG_PRESS, TOUCH_GESTURE_LONG_PRESS),
                ev.getOccurredTime() + 500);
        }
        mHandler.sendTimingEvent(InnerEvent.get(SHOW_PRESS), ev.getOccurredTime() + TAP_TIMEOUT);
        handled |= mListener.onDown(ev);
        return handled;
    }

    private void calledOut() {
        mVelocityDetector.clear();
        mVelocityDetector = null;

        mIsDoubleTapping = false;
        mDeferConfirmSingleTap = false;
        mIgnoreNextUpEvent = false;
        mHandler.removeEvent(SHOW_PRESS);
        mHandler.removeEvent(LONG_PRESS);
    }

    private boolean isHandled(TouchEvent ev) {
        boolean handled;
        handled = mListener.onSingleTapUp(ev);
        if (mDeferConfirmSingleTap && mDoubleTapListener != null) {
            mDoubleTapListener.onSingleTapConfirmed(ev);
        }
        return handled;
    }

    private boolean isHandled(TouchEvent ev, boolean handled) {
        mVelocityDetector.calculateCurrentVelocity(1000, mMaximumFlingVelocity, mMaximumFlingVelocity);
        float velocityX = mVelocityDetector.getHorizontalVelocity();
        float velocityY = mVelocityDetector.getVerticalVelocity();

        if ((Math.abs(velocityY) > mMinimumFlingVelocity)
            || (Math.abs(velocityX) > mMinimumFlingVelocity)) {
            handled = mListener.onFling(mCurrentDownEvent, ev, velocityX, velocityY);
        }
        return handled;
    }

    private void cancel() {
        mHandler.removeEvent(SHOW_PRESS);
        mHandler.removeEvent(LONG_PRESS);
        mHandler.removeEvent(TAP);
        mVelocityDetector.clear();
        mVelocityDetector = null;
        mIsDoubleTapping = false;
        mStillDown = false;
        mAlwaysInTapRegion = false;
        mAlwaysInBiggerTapRegion = false;
        mDeferConfirmSingleTap = false;
        mInLongPress = false;
        mInContextClick = false;
        mIgnoreNextUpEvent = false;
    }

    private void cancelTaps() {
        mHandler.removeEvent(SHOW_PRESS);
        mHandler.removeEvent(LONG_PRESS);
        mHandler.removeEvent(TAP);
        mIsDoubleTapping = false;
        mAlwaysInTapRegion = false;
        mAlwaysInBiggerTapRegion = false;
        mDeferConfirmSingleTap = false;
        mInLongPress = false;
        mInContextClick = false;
        mIgnoreNextUpEvent = false;
    }

    private boolean isConsideredDoubleTap(MmiPoint firstDown, long firstUp, MmiPoint secondDown, long secondDownTime) {
        if (!mAlwaysInBiggerTapRegion) {
            return false;
        }
        final long deltaTime = secondDownTime - firstUp;
        if (deltaTime > DOUBLE_TAP_TIMEOUT || deltaTime < DOUBLE_TAP_MIN_TIME) {
            return false;
        }

        int deltaX = (int) firstDown.getX() - (int) secondDown.getX();
        int deltaY = (int) firstDown.getY() - (int) secondDown.getY();
        int slopSquare = mDoubleTapSlopSquare;
        return (deltaX * deltaX + deltaY * deltaY < slopSquare);
    }

    /**
     * Analyzes the given generic motion event and if applicable triggers the
     * appropriate callbacks on the {@link OnGestureListener} supplied.
     *
     * @param ev The current motion event.
     * @return true if the {@link OnGestureListener} consumed the event,
     * else false.
     */
    public boolean onGenericMotionEvent(TouchEvent ev) {
        return false;
    }

    private void dispatchLongPress() {
        mHandler.removeEvent(TAP);
        mDeferConfirmSingleTap = false;
        mInLongPress = true;
        mListener.onLongPress(mCurrentDownEvent);
    }
}
